// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

// Doc（通常作为go-Doc运行）接受零个、一个或两个参数。
// None
// 零参数：
// 医生
// 在当前目录中显示包的文档。
// None
// 一个论点：
// go doc<pkg>
// 转到文档<sym>[.<methodOrField>]
// go doc[<pkg>]<sym>[.<methodOrField>]
// go doc[<pkg>][<sym>]<methodOrField>
// 此列表中成功的第一项是其文档
// 是印刷的。如果有符号但没有包，则当前
// 已选择目录。但是，如果论点以大写字母开头
// 字母它始终假定为当前目录中的符号。
// None
// 两个论点：
// go doc<pkg><sym>[.<methodOrField>]
// None
// 显示包、符号和方法或字段的文档。这个
// 第一个参数必须是完整的包路径。这类似于
// godoc命令的命令行用法。
// None
// 对于命令，除非存在-cmd标志“go doc命令”
// 仅显示包的包级文档。
// None
// -src标志使doc打印符号的完整源代码，例如
// 作为结构、函数或方法的主体。
// None
// -all标志使doc打印包和文件的所有文档
// 所有可见的符号。参数必须标识一个包。
// None
// 要获得完整的文档，请运行“go help doc”。
package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/build"
	"go/token"
	"io"
	"log"
	"os"
	"path"
	"path/filepath"
	"strings"
)

var (
	unexported bool // -美国国旗
	matchCase  bool // -c旗
	showAll    bool // -全旗
	showCmd    bool // -cmd标志
	showSrc    bool // -src标志
	short      bool // -短旗
)

// 用法是flags包的替换用法函数。
func usage() {
	fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n")
	fmt.Fprintf(os.Stderr, "\tgo doc\n")
	fmt.Fprintf(os.Stderr, "\tgo doc <pkg>\n")
	fmt.Fprintf(os.Stderr, "\tgo doc <sym>[.<methodOrField>]\n")
	fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.]<sym>[.<methodOrField>]\n")
	fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.][<sym>.]<methodOrField>\n")
	fmt.Fprintf(os.Stderr, "\tgo doc <pkg> <sym>[.<methodOrField>]\n")
	fmt.Fprintf(os.Stderr, "For more information run\n")
	fmt.Fprintf(os.Stderr, "\tgo help doc\n\n")
	fmt.Fprintf(os.Stderr, "Flags:\n")
	flag.PrintDefaults()
	os.Exit(2)
}

func main() {
	log.SetFlags(0)
	log.SetPrefix("doc: ")
	dirsInit()
	err := do(os.Stdout, flag.CommandLine, os.Args[1:])
	if err != nil {
		log.Fatal(err)
	}
}

// do是一种工作方式，它从主要部件中分离出来，使测试更容易。
func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
	flagSet.Usage = usage
	unexported = false
	matchCase = false
	flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported")
	flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)")
	flagSet.BoolVar(&showAll, "all", false, "show all documentation for package")
	flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command")
	flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol")
	flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol")
	flagSet.Parse(args)
	var paths []string
	var symbol, method string
	// 循环直到有东西打印出来。
	dirs.Reset()
	for i := 0; ; i++ {
		buildPackage, userPath, sym, more := parseArgs(flagSet.Args())
		if i > 0 && !more { // 忽略第一次迭代中的“更多”位。
			return failMessage(paths, symbol, method)
		}
		if buildPackage == nil {
			return fmt.Errorf("no such package: %s", userPath)
		}
		symbol, method = parseSymbol(sym)
		pkg := parsePackage(writer, buildPackage, userPath)
		paths = append(paths, pkg.prettyPath())

		defer func() {
			pkg.flush()
			e := recover()
			if e == nil {
				return
			}
			pkgError, ok := e.(PackageError)
			if ok {
				err = pkgError
				return
			}
			panic(e)
		}()

		// 内置包装需要特殊处理：其符号较低
		// 但我们总是想看到他们。
		if pkg.build.ImportPath == "builtin" {
			unexported = true
		}

		// 我们有一个包裹。
		if showAll && symbol == "" {
			pkg.allDoc()
			return
		}

		switch {
		case symbol == "":
			pkg.packageDoc() // 包存在，所以我们得到了一些输出。
			return
		case method == "":
			if pkg.symbolDoc(symbol) {
				return
			}
		default:
			if pkg.methodDoc(symbol, method) {
				return
			}
			if pkg.fieldDoc(symbol, method) {
				return
			}
		}
	}
}

// 当没有结果显示时，failMessage会创建格式良好的错误消息。
func failMessage(paths []string, symbol, method string) error {
	var b bytes.Buffer
	if len(paths) > 1 {
		b.WriteString("s")
	}
	b.WriteString(" ")
	for i, path := range paths {
		if i > 0 {
			b.WriteString(", ")
		}
		b.WriteString(path)
	}
	if method == "" {
		return fmt.Errorf("no symbol %s in package%s", symbol, &b)
	}
	return fmt.Errorf("no method or field %s.%s in package%s", symbol, method, &b)
}

// parseArgs分析参数（如果有）并返回包
// 它表示用户用于标识的参数部分
// 路径（如果是当前包，则为“”）和符号
// （可能使用.method）在该包中。
// parseSymbol用于分析符号本身。
// 布尔最终参数报告是否可能
// 可能还有更多的目录值得一看。只会
// 如果包路径与某个目录部分匹配，则为true
// 而且可能会有更多的比赛。例如，如果参数
// 是rand.Float64，我们必须扫描crypto/rand和math/rand
// 要查找符号，第一次调用将返回crypto/rand，true。
func parseArgs(args []string) (pkg *build.Package, path, symbol string, more bool) {
	wd, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}
	if len(args) == 0 {
		// 简单：当前目录。
		return importDir(wd), "", "", false
	}
	arg := args[0]
	// 我们有一场争论。如果是以开头的目录名。或
	// 使用绝对路径名。这将“/errors”与“errors”区分开来
	// 如果当前目录包含非标准错误包。
	if isDotSlash(arg) {
		arg = filepath.Join(wd, arg)
	}
	switch len(args) {
	default:
		usage()
	case 1:
		// 完成如下。
	case 2:
		// 包必须是可查找和可导入的。
		pkg, err := build.Import(args[0], wd, build.ImportComment)
		if err == nil {
			return pkg, args[0], args[1], false
		}
		for {
			packagePath, ok := findNextPackage(arg)
			if !ok {
				break
			}
			if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil {
				return pkg, arg, args[1], true
			}
		}
		return nil, args[0], args[1], false
	}
	// 通常情况：一个论点。
	// 如果它包含斜杠，则以包路径开始
	// 或绝对目录。
	// 首先，它是一个完整的包路径吗？如果是这样，我们就完成了。
	// 这避免了对具有其他特性的包路径的混淆
	// 包路径作为其前缀。
	var importErr error
	if filepath.IsAbs(arg) {
		pkg, importErr = build.ImportDir(arg, build.ImportComment)
		if importErr == nil {
			return pkg, arg, "", false
		}
	} else {
		pkg, importErr = build.Import(arg, wd, build.ImportComment)
		if importErr == nil {
			return pkg, arg, "", false
		}
	}
	// 另一个消除歧义的方法是：如果参数以大写字母开头
	// 大小写字母，它只能是当前目录中的符号。
	// 消除不区分大小写的文件系统引起的问题
	// 将大写名称与包名匹配。
	if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) {
		pkg, err := build.ImportDir(".", build.ImportComment)
		if err == nil {
			return pkg, "", arg, false
		}
	}
	// 如果它有一个斜杠，它必须是一个包路径，但有一个符号。
	// 这是我们关心的最后一个包路径。
	slash := strings.LastIndex(arg, "/")
	// 在斜杠之前或之后，包路径中可能有句点
	// 在符号和方法之间。
	// 在不同的时间段拆分字符串以查看我们发现的内容。
	// 一般来说，可能会有歧义，但这几乎总是应该的
	// 工作
	var period int
	// 斜杠+1：如果没有斜杠，则值为-1，起点为0；否则
	// start是斜杠后面的字节。
	for start := slash + 1; start < len(arg); start = period + 1 {
		period = strings.Index(arg[start:], ".")
		symbol := ""
		if period < 0 {
			period = len(arg)
		} else {
			period += start
			symbol = arg[period+1:]
		}
		// 我们已经确定了一个包裹了吗？
		pkg, err := build.Import(arg[0:period], wd, build.ImportComment)
		if err == nil {
			return pkg, arg[0:period], symbol, false
		}
		// 查看是否有包的basename或tail，如用于编码/json的json
		// 或ivy/value for robpike.io/ivy/value。
		pkgName := arg[:period]
		for {
			path, ok := findNextPackage(pkgName)
			if !ok {
				break
			}
			if pkg, err = build.ImportDir(path, build.ImportComment); err == nil {
				return pkg, arg[0:period], symbol, true
			}
		}
		dirs.Reset() // for循环的下一次迭代必须再次扫描所有目录。
	}
	// 如果它有斜线，我们就失败了。
	if slash >= 0 {
		// build.Import应始终在其错误消息中包含路径，
		// 我们应该避免重蹈覆辙。不幸的是，build.Import没有
		// 返回结构化错误。这不容易修复，因为
		// 调用“go list”并从加载的包返回错误文本。
		// TODO（golang.org/issue/34750）：使用golang.org/x/tools/go/packages加载
		// 而不是去建造。
		importErrStr := importErr.Error()
		if strings.Contains(importErrStr, arg[:period]) {
			log.Fatal(importErrStr)
		} else {
			log.Fatalf("no such package %s: %s", arg[:period], importErrStr)
		}
	}
	// 我猜它是当前目录中的一个符号。
	return importDir(wd), "", arg, false
}

// dotPaths列出了类Unix和Unix上的所有点路径
// 类似Windows的文件系统。我们检查了所有的，作为机会
// 错误的概率很小，即使在Windows上，人们也会使用/
// 有时
var dotPaths = []string{
	`./`,
	`../`,
	`.\`,
	`..\`,
}

// isDotSlash报告路径是否以引用开始
// 给当地人。或目录
func isDotSlash(arg string) bool {
	if arg == "." || arg == ".." {
		return true
	}
	for _, dotPath := range dotPaths {
		if strings.HasPrefix(arg, dotPath) {
			return true
		}
	}
	return false
}

// importDir只是捕获build.importDir的包装器时出错。
func importDir(dir string) *build.Package {
	pkg, err := build.ImportDir(dir, build.ImportComment)
	if err != nil {
		log.Fatal(err)
	}
	return pkg
}

// parseSymbol将str分解为符号和方法。
// 两者都可能缺失或方法可能缺失。
// 如果存在，每个都必须是有效的Go标识符。
func parseSymbol(str string) (symbol, method string) {
	if str == "" {
		return
	}
	elem := strings.Split(str, ".")
	switch len(elem) {
	case 1:
	case 2:
		method = elem[1]
	default:
		log.Printf("too many periods in symbol specification")
		usage()
	}
	symbol = elem[0]
	return
}

// isExported报告名称是否为导出的标识符。
// 如果未报告的标志（-u）为true，isExported将返回true，因为
// 这意味着我们将该名称视为已导出。
func isExported(name string) bool {
	return unexported || token.IsExported(name)
}

// findNextPackage返回下一个与
// （可能是部分）包路径pkg。布尔值报告是否找到任何匹配项。
func findNextPackage(pkg string) (string, bool) {
	if filepath.IsAbs(pkg) {
		if dirs.offset == 0 {
			dirs.offset = -1
			return pkg, true
		}
		return "", false
	}
	if pkg == "" || token.IsExported(pkg) { // 大写符号不能是包名称。
		return "", false
	}
	pkg = path.Clean(pkg)
	pkgSuffix := "/" + pkg
	for {
		d, ok := dirs.Next()
		if !ok {
			return "", false
		}
		if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) {
			return d.dir, true
		}
	}
}

var buildCtx = build.Default

// splitGopath将$GOPATH拆分为根列表。
func splitGopath() []string {
	return filepath.SplitList(buildCtx.GOPATH)
}
